The Skyquad
2015-09-15
A skyquad is to a quad what a skybox is to a box. The skyquad renders the sky by sending just a single quad down the rendering pipeline. The skyquad is useful not only in OpenGL, but also in ray tracing, where we have rays passing through an image plane. It was in this latter context where I first implemented a skyquad, so this post begins with the general mathematical machinery for a ray tracer and then applies it to OpenGL.
The Ray Equation
In a ray tracer, we have a camera and a window into the world - the image to be rendered - and we shoot rays passing through each the pixel of the image:

We can describe the problem of determining the ray through a given point in the image as follows:
Input:
, , and - the camera’s up, right, and forward vectors, respectively. - the camera’s position.- fovy - the camera’s vertical field of view angle.
- the width and height of the image, respectively. - the coordinates of the target point in the image, ranging from to .
Output:
- The ray
originating at and passing through .
We adopt the following conventions:
corresponds to the bottom-left corner of the image. corresponds to the top-right corner of the image.
Note that
Determining the distance to the image plane
The first step is determining the distance

From the image, we can see that:
Solving for
Deriving the ray equation
Next, from the first figure, we can observe that the center of the image is determined by:
To reach the target point
Deriving
The vector from the camera position
Finally, the ray equation is
The Skyquad in OpenGL
To draw the skyquad in OpenGL, we render a quad directly in NDC space
[1], with
To simplify our calculations, we compute the ray direction in NDC
space and then apply the viewport’s aspect ratio
, , and , assuming a right-handed coordinate system. = 1 (send quad to the background)
Using the properties above, the distance to the image plane
The ray’s direction vector in NDC space is:
Next, apply the viewport’s aspect ratio
And finally, normalise the vector and apply the camera’s rotation
matrix
To render the skyquad, we send a 2D quad with coordinates ranging
from
- We do not need to compute a ray per pixel; instead, we can compute a ray for every vertex of the quad and let the pipeline interpolate the rays.
- Cubemap sampling does not require the vector to be normalized. Therefore, to make the shader more efficient, we do not normalize the sky ray.
Vertex shader
uniform mat3 Rotation;
uniform float fovy;
uniform float aspect;
layout (location = 0) in vec2 Position;
out vec3 Ray;
vec3 skyRay (vec2 Texcoord)
{
float d = 0.5 / tan(fovy/2.0);
return vec3((Texcoord.x - 0.5) * aspect,
Texcoord.y - 0.5,
-d);
}
void main ()
{
Ray = Rotation * skyRay(Position*0.5 + 0.5); // map [-1,1] -> [0,1]
gl_Position = vec4(Position, 0.0, 1.0);
}
Fragment shader
uniform samplerCube tex;
in vec3 Ray;
layout (location = 0) out vec4 Colour;
void main ()
{
vec3 R = normalize(Ray);
Colour = vec4(pow(texture(tex, R).rgb, vec3(1.0/2.2)), 1.0);
}
Certainly a lot of maths for such a simple shader, but that is the beauty of computer graphics. The final solution is simple and works for both ray tracing and OpenGL. Below is a screenshot produced by a skyquad:

[1] Technically it is clip space, but since we set